Skip to content

test: cover modules with an admin and a blog module - #176

Open
protibimbok wants to merge 4 commits into
masterfrom
feat/modular
Open

protibimbok wants to merge 4 commits into
masterfrom
feat/modular

Conversation

@protibimbok

Copy link
Copy Markdown
Member

Summary

The framework's modular feature (goravel/framework#1613, goravel/vite-plugin#4) gets a working example here: two modules in modules/, wired the way an application would wire them, and feature tests that pin the behaviour.

  • modules/admin is a small Vue admin that other modules extend. It shows how a module can define a capability of its own, which other modules implement, on top of the framework's.
  • modules/blog uses most framework capabilities (routes, API routes, config, migrations, views, translations, Vite entries) and plugs into the admin.

The admin is modelled on laramod-admin.

What's in it

Enabling modules

The unedited output of artisan modular:init: WithModules(Modules) in bootstrap/app.go, bootstrap/modules.go, config/modular.go and the go:generate ... modular:publish line in main.go. Plus facades.Modular() in app/facades.

The admin module

The DashboardScripts capability. A module takes part in the admin by implementing:

// modules/admin/contracts
type ProvidesDashboardScripts interface {
	DashboardScripts() []string // e.g. "resources/js/dashboard.ts"
}
  • dashboard.Entries(registry) collects every module's scripts in module order, then the admin's own resources/js/main.ts, so every module has registered before the admin mounts.
  • A script must also be listed in the module's ViteEntries(), because that is what gets it built. A missing one fails with dashboard script [x] of module [y] is not in its ViteEntries().
  • /admin and /admin/*page render one page. Every entry goes through a single facades.Vite().Tags(...) call, so shared chunks are preloaded once. The path comes from modules.admin.path.
  • The admin has Order() -1, so its catch-all route comes after every other module's routes.

Frontend (modules/admin/resources/js, Vue):

Piece What it does
registerModule({ id, order, menu, routes, fills }) A module adds menu groups, pages and always-rendered fills. The registry is sealed before mount, so a late registration throws instead of silently not showing.
createSlot(name) → Provider / Outlet / Fill Slot/fill: a Fill teleports its children into the Outlet while both are mounted, in mount order.
DashboardWidgets, HeaderActions The two built-in slots: the dashboard grid, and the top right of the header.
AdminLink, navigate Client-side routing beneath the admin's base path. A refresh works because the server answers every URL beneath it.

The blog module

  • GET /blog: a module view (blog/index.tmpl) with a module translation (blog/messages.title).
  • GET|POST /api/blog/posts: lists the latest posts with a total, and creates posts with validation (422 with errors).
  • The blog_posts migration is generated with make:migration --module=blog and registered on the module struct.
  • DashboardScripts() returns resources/js/dashboard.ts, which registers:
    • Posts and New post pages,
    • a "New post" button in HeaderActions, shown only on the Posts page,
    • a post-count card in DashboardWidgets.

Tests

  • ModularTestSuite:
    • registry order and paths, and the blog's capabilities,
    • modules.* config and module translations,
    • modular:list,
    • the modules field of vite:inputs,
    • the blog API and page against the database (module migrations run with the app's).
  • AdminDashboardTestSuite:
    • dashboard.Entries, table-driven: module order, modules without scripts skipped, a script missing from ViteEntries(), no admin registered,
    • the rendered /admin, /admin/blog and /admin/blog/new pages from a fixture manifest,
    • a 500 when nothing is built.
  • VitePluginTestSuite:
    • the build also checks the module entries,
    • the dev server's hot file is now JSON, with the alias table (@, @blog, @admin) pinned,
    • @blog/dashboard.ts resolves through the dev server.

Dependencies / before merging

How to try it

npm install                     # links ../vite-plugin; run `npm run build` there first
npm run build                   # builds modules/blog/.../dashboard.ts and modules/admin/.../main.ts
go run . artisan migrate
go run .                        # open /admin, /admin/blog, /admin/blog/new, /blog

go test ./tests/feature -run 'TestModularTestSuite|TestAdminDashboardTestSuite|TestVite'

Output of `artisan modular:init`: WithModules(Modules) in
bootstrap/app.go,
an empty bootstrap/modules.go, config/modular.go, modules/.gitkeep and
the
go:generate modular:publish line in main.go. Adds facades.Modular() to
app/facades.
modules/admin is a Vue admin that other modules fill, modelled on
laramod-admin:
- contracts.ProvidesDashboardScripts: a module lists Vite entries the
  admin
  page loads before its own main.ts; each must also be in ViteEntries().
- dashboard.Entries collects them in module order and fails on a script
  that is not a Vite entry. The page renders them with one Vite Tags
  call.
- /admin and /admin/*page serve the page; a small client router, a
  module
  registry (registerModule with menu, routes, fills) and a slot/fill
  mechanism (createSlot: Provider, Outlet, Fill via Teleport) with two
  slots, DashboardWidgets and HeaderActions.

modules/blog uses most capabilities (routes, API routes, config,
migrations, views, translations, Vite entries) and implements
ProvidesDashboardScripts: its dashboard.ts registers the Posts and New
post
pages, fills HeaderActions with a "New post" button and DashboardWidgets
with a post count card.

tsconfig.json type-checks module scripts and maps @admin/*.
ModularTestSuite: registry order and paths, capabilities, module config
and translations, modular:list, the modules field of vite:inputs, the
blog
API and page against the database.

AdminDashboardTestSuite: dashboard.Entries (order, skipped modules, a
script missing from ViteEntries, no admin) and the rendered admin page
from a fixture manifest.
@protibimbok
protibimbok requested a review from a team as a code owner November 14, 2026 09:23

@hwbrzzl hwbrzzl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Could you simplify the examples? There are a lot of unnecessary files for an example.

Comment thread main.go
"goravel/bootstrap"
)

//go:generate go run . artisan modular:publish

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why need this line?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature exists for third party modules. If the module source does not live in the project dir then vite cannot work properly (building, figuring out the path). Similar logic applies for language files, views etc. We need an way to bring them into project dir.
go provides a generate command.

The go generate command is a built-in Go tool that automates running code-generation programs directly from instructions embedded in your Go source files. It is designed for package authors to reduce repetitive boilerplate code, and it must be run manually during development rather than automatically during go build

We are using it to make the integration easier.
People will be able to build individual parts like auth, blog, chat - publish and re-use them without requiring to change anything.

Comment thread app/facades/modular.go
"github.com/goravel/framework/contracts/modular"
)

func Modular() modular.Registry {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about 'Module'?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean Module.Registry() ?

Comment thread modules/admin/module.go
}

// Order -1 makes the admin the last module, so its routes come after the others'.
func (m *Module) Order() int {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not respect the module slice order in the bootstrap/modules.go file? Then this function can be removed entirely.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make:module inserts in the end, and some app may require this ordering i.e. put themselves in the end. users don't need to be aware of that order unless necessary. this is also optional, so no need to implement if not necessary

// admin's own router decides which page is shown, so a refresh on
// /admin/blog/posts works.
func Web(router route.Router) {
base := "/" + strings.Trim(facades.Config().GetString("modules.admin.path", "admin"), "/")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about

Suggested change
base := "/" + strings.Trim(facades.Config().GetString("modules.admin.path", "admin"), "/")
base := "/" + strings.Trim(facades.Config().Module(admin).GetString("path", "admin"), "/")

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you mean .Module("admin")?
If no, where does admin come from

base := "/" + strings.Trim(facades.Config().GetString("modules.admin.path", "admin"), "/")
controller := controllers.NewDashboardController()

router.Get(base, controller.Show)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can facades.Route be used here directly?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, but not recommended as it may bypass some grouping behavior.

Comment thread modules/blog/module.go
routes.Web(router)
}

func (m *Module) ApiRoutes(router route.Router) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function can be merged with Routes.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auth module may handle authentication differently in api & web routes.
different middlewares may be applied based on api & web.
how do we handle that?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants